package cn.ycc1.functionlibrary.reflection;

/**
 * Reading Annotations
 * @author ycc
 * @date 2025/3/9
 * Annotations are widely used with the Reflection API. Many frameworks use them extensively and with great success.
 * This is the case for the object relational mapping frameworks, the dependency Injection frameworks, some validation frameworks,
 * some security frameworks. Basically all Java EE, Jakarta EE, and all the similar frameworks define their sets of annotations
 * that you can add on classes and class members to trigger behavior. This section shows you how all this is working under the
 * hood, and you can use annotations at runtime.
 */
public class ReadingAnnotations {
    /**
     * Discovering Annotations on Elements
     * The page Annotations explains what are annotations, and how you can use them.
     *
     * The following classes from the Reflection API implement the interface AnnotatedElement that gives you access to the
     * annotations such an element can carry: Field, Method, Constructor, and Class. Remember that the class Class models
     * classes, abstract classes, interfaces, enumerations, records, and arrays.
     *
     * This AnnotatedElement gives you the methods you need to discover the annotations added on the corresponding element,
     * as well as the instance of this annotation, so that you can call its methods.
     *
     * Suppose that you have the following enumeration, and the two annotations @Bean and @Serialized in your application.
     *
     * enum SerializedFormat {
     *     BINARY, XML, JSON
     * }
     *
     * @Target({ElementType.TYPE})
     * @Retention(RetentionPolicy.RUNTIME)
     * @interface Bean {}
     *
     * @Target({ElementType.TYPE})
     * @Retention(RetentionPolicy.RUNTIME)
     * @interface Serialized {
     *     SerializedFormat format() default SerializedFormat.JSON;
     * }
     * As you can see, both can be applied on types, and are made available at runtime. The @Serialized annotation defines an
     * attribute, format , that can take three values: BINARY, XML, and JSON.
     *
     * Now, suppose that you have a Person class, declared in this way.
     *
     * @Serialized @Bean
     * public class Person {
     * }
     * The Reflection API gives you several methods to discover these annotations.
     *
     * isAnnotationPresent(Class<? extends Annotation>): allows you to check is a given annotation is present on this element.
     * The class you need to pass as an argument must be an annotation class. If you don't, you will get a compiler error.
     * getAnnotations() and getDeclaredAnnotations(): gives you an array with the annotations present on this element. This
     * array can be empty if there is no such annotation. The getDeclaredAnnotations() gives you the annotations declared on
     * this element only, without the inherited annotations.
     * getAnnotation(Class) and getDeclaredAnnotation(Class): returns an annotation for the specific type passed as an argument.
     * The method getDeclaredAnnotation(Class) only returns the annotation declared on this element, without the inherited
     * annotations.
     * getAnnotationsByType(Class has been added in Java SE 8 to support repeating annotations. This method returns an array of
     * the annotations associated with this element. If it finds a repeating annotation, it opens it to return the repeating
     * element.
     * Let us see these methods in action.
     *
     * First, you can check if the Person class has any annotation, and discover them.
     *
     * Class<?> c = Person.class;
     *
     * boolean isBean = c.isAnnotationPresent(Bean.class);
     * System.out.println("isBean = " + isBean);
     *
     * Annotation[] annotations = c.getAnnotations();
     * for (Annotation annotation : annotations) {
     *     System.out.println("annotation = " + annotation);
     * }
     * Running the previous code prints the following.
     *
     * isBean = true
     * annotation = @org.devjava.Serialized(format=JSON)
     * annotation = @org.devjava.Bean()
     * Note that what you get in return are instances of the annotation classes you define, on which you can call the methods
     * they declare. Let us now run this code.
     *
     * Class<?> c = Person.class;
     *
     * Serialized annotation = c.getAnnotation(Serialized.class);
     * SerializedFormat format = annotation.format();
     * System.out.println("format = " + format);
     * Running the previous code prints the following.
     *
     * format = JSON
     * Let us create another, repeating annotation.
     *
     * enum ValidationRules {
     *     NON_NULL, NON_EMPTY, NON_ZERO
     * }
     *
     * @Target(ElementType.FIELD)
     * @Retention(RetentionPolicy.RUNTIME)
     * @interface Validators {
     *     Validator[] value();
     * }
     *
     * @Target(ElementType.FIELD)
     * @Repeatable(Validators.class)
     * @interface Validator {
     *     ValidationRules value();
     * }
     * And modify the Person class, to add a field to it, with this repeating annotation.
     *
     * public class Person {
     *
     *     @Validator(ValidationRules.NON_NULL)
     *     @Validator(ValidationRules.NON_EMPTY)
     *     private String name;
     * }
     * From the class file perspective, there is actually only one annotation on the name field: @Validators. You can then get
     * this instance, and get the enclosed @Validator annotations by calling the value() method. This is what the following
     * example is doing.
     *
     * Class<?> c = Person.class;
     * Field nameField = c.getDeclaredField("name");
     *
     * Annotation[] annotations = nameField.getAnnotations();
     * System.out.println("# annotations = " + annotations.length);
     *
     * Validators validators = (Validators)annotations[0];
     * for (Validator validator : validators.value()) {
     *     System.out.println("validator = " + validator);
     * }
     * Running the previous example prints the following.
     *
     * # annotations = 1
     * validator = @org.devjava.Validator(NON_NULL)
     * validator = @org.devjava.Validator(NON_EMPTY)
     * So you can have access to repeating annotations using the getAnnotations() method, but it's a little tedious.
     *
     * This is where the getAnnotationsByType(Class) can make your code simpler. Calling this method with the Validator class
     * as an argument gives you the annotations you are looking for.
     *
     * Class<?> c = Person.class;
     * Field nameField = c.getDeclaredField("name");
     *
     * Annotation[] annotations = nameField.getAnnotationsByType(Validator.class);
     * System.out.println("# annotations = " + annotations.length);
     *
     * for (Annotation annotation : annotations) {
     *     System.out.println("annotation = " + annotation);
     * }
     * Running the previous example gives you the following.
     *
     * # annotations = 2
     * annotation = @org.devjava.Validator(NON_NULL)
     * annotation = @org.devjava.Validator(NON_EMPTY)
     */

    /**
     * Getting Inherited Annotations
     * Annotations declared on types (classes, abstract classes, and interfaces) can be inherited by their subtypes. It does not
     * make sense to do that for enumerations and records, as these two are final classes, and thus cannot be extended.
     *
     * The methods getDeclaredAnnotations() and getDeclaredAnnotation(Class) have a strict behavior. They return only the
     * annotation declared on the type you are calling these methods on. On the other hand, getAnnotations() and
     * getAnnotation(Class) may return inherited annotations. You can see that on the following example.
     *
     * Suppose you have the following Bean annotation, that can be inherited.
     *
     * @Target(ElementType.TYPE)
     * @Retention(RetentionPolicy.RUNTIME)
     * @Inherited
     * @interface Bean {}
     * And the following two classes. Note that the Person class is annotated with @Bean, and not the User class that extends it.
     *
     * @Bean
     * class Person {}
     *
     * class User extends Person {}
     * You can examine the available annotations on the User class with the following code.
     *
     * Class<?> c = Employee.class;
     *
     * Annotation[] declaredAnnotations = c.getDeclaredAnnotations();
     * System.out.println("# declared annotations = " + declaredAnnotations.length);
     *
     * Annotation[] annotations = c.getAnnotations();
     * System.out.println("# annotations = " + annotations.length);
     * for (Annotation annotation : annotations) {
     *     System.out.println("annotation = " + annotation);
     * }
     * Running the previous code prints the following.
     *
     * # declared annotations = 0
     * # annotations = 1
     * annotation = @org.devjava.Bean()
     */

    /**
     * Getting Annotations on Types
     * Annotated types are annotations declared on types, that can be used anywhere in your code. You can see example of such
     * use of types on this page.
     *
     * The Reflection API gives you access to some of these annotations using through a specific interface: AnnotatedType.
     *
     * Suppose that you have the following annotation. Note that this annotation can be used where the types are used, not the
     * types themselves.
     *
     * @Target(ElementType.TYPE_USE)
     * @Retention(RetentionPolicy.RUNTIME)
     * @interface NonNull {}
     * You can the use this annotation in this way. Note that this annotation is declared on the extending class, and on an
     * implemented interface.
     *
     * public class Person {}
     *
     * public class User
     *         extends @NonNull Person
     *         implements @NonNull Serializable {
     * }
     * The Reflection API gives you access to this annotation in ths way.
     *
     * Class<?> c = Employee.class;
     *
     * AnnotatedType superClass = c.getAnnotatedSuperclass();
     * Annotation[] superClassAnnotations = superClass.getAnnotations();
     * for (Annotation annotation : superClassAnnotations) {
     *     System.out.println("annotation on the super class = " + annotation);
     * }
     *
     * AnnotatedType[] interfaces = c.getAnnotatedInterfaces();
     * for (AnnotatedType implementedInterface : interfaces) {
     *     Annotation[] interfaceAnnotations = implementedInterface.getAnnotations();
     *     for (Annotation interfaceAnnotation : interfaceAnnotations) {
     *         System.out.println("annotation on the implemented interface = " + interfaceAnnotation);
     *     }
     * }
     * Running the previous example gives you the following.
     *
     * annotation on the super class = @org.devjava.NonNull()
     * annotation on the implemented interface = @org.devjava.NonNull()
     * Let us consider the following example, where you can discover an annotation on an exception. This exception is not a
     * RuntimeException, so you need to handle it explicitly. To overcome this, you decide to create an annotation to tell your
     * client code if it can rethrow this exception as a runtime exception.
     *
     * class EmptyStringException extends Exception {
     *     public EmptyStringException(String message) {
     *         super(message);
     *     }
     * }
     *
     * @Target(ElementType.TYPE_USE)
     * @Retention(RetentionPolicy.RUNTIME)
     * public @interface ReThrowAsRuntimeException {}
     *
     * public class Message {
     *     private final String name;
     *
     *     public Message(String name) throws @ReThrowAsRuntimeException EmptyStringException {
     *         Objects.requireNonNull(name);
     *         if (name.isEmpty()) {
     *             throw new EmptyStringException("name is empty");
     *         }
     *         this.name = name;
     *     }
     *
     *     public String name() {
     *         return name;
     *     }
     * }
     * You can then use this class with the following client code.
     *
     * try {
     *
     *     Message message = new Message("");
     *
     * } catch (EmptyStringException e) {
     *
     *     Class<?> c = Message.class;
     *     Constructor<?> constructor = c.getConstructor(String.class);
     *
     *     AnnotatedType[] annotatedExceptionTypes = constructor.getAnnotatedExceptionTypes();
     *     for (AnnotatedType annotatedExceptionType : annotatedExceptionTypes) {
     *         boolean annotationPresent =
     *                 annotatedExceptionType.isAnnotationPresent(ReThrowAsRuntimeException.class);
     *         throw new RuntimeException(e);
     *     }
     *     System.out.println(e.getMessage());
     *     e.printStackTrace();
     * }
     * Running the previous code prints the following. As you can see, the exception is rethrown as a runtime exception.
     *
     * Exception in thread "main" java.lang.RuntimeException: org.devjava.EmptyStringException: name is empty
     *     at org.devjava.Main.main(ScrapMain.java:32)
     * Caused by: org.devjava.EmptyStringException: name is empty
     *     at org.devjava.Message.<init>(ScrapMain.java:18)
     *     at org.devjava.Main.main(ScrapMain.java:45)
     */

}
